package com.magenta.maxoptra.gis.service;

import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.commons.Pair;
import com.magenta.maxoptra.gis.commons.Quadruple;
import com.magenta.maxoptra.gis.commons.Triple;
import com.magenta.maxoptra.gis.util.Apis;
import com.magenta.maxoptra.gis.utils.SpatialDataHelper;
import com.magenta.maxoptra.routing.GeoProviderAccessStatistics;
import org.apache.commons.lang.BooleanUtils;
import org.jetbrains.annotations.Nullable;

import javax.annotation.Resource;
import javax.ejb.*;
import javax.jms.*;
import javax.jms.Queue;
import javax.xml.ws.soap.SOAPFaultException;
import java.lang.IllegalStateException;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static com.magenta.maxoptra.gis.GisParameter.enableCache;
import static com.magenta.maxoptra.gis.GisParameter.enableLogging;
import static com.magenta.maxoptra.gis.utils.GeoProviderSettingsUtils.isExpiredCacheEntry;

/**
 * User: KuznetsovD
 * Date: 28.06.2010
 * Time: 11:47:03
 */
@Local(SyncGeoRoutingService.class)
@TransactionManagement(TransactionManagementType.BEAN)
public class SyncRoutingServiceImpl implements SyncGeoRoutingService, RoutingServiceMBean {

    @EJB
    PersistedCache persistedCache;

    @EJB
    SegmentedCache segmentedCache;

    @EJB
    GeoProviderFacade geoProviderFacade;

    @EJB
    ProvidersRegistry geoProvidersRegistry;

    @Resource(mappedName = "queue/persisted_cache")
    Queue persisted_cache_queue;

    @Resource(mappedName = "ConnectionFactory")
    QueueConnectionFactory factory;

    ThreadLocal<Integer> attemptsCountForBatchDistanceCalculation = new ThreadLocal<Integer>();

    protected Logger logger = new Logger(getClass());

    protected ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 3, 0, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(3));

    private boolean isCacheEnabled(GeoProviderSettings settings) {
        if (settings != null && settings.getGisParams() != null) {
            String enabled = settings.getGisParam(enableCache.withProvider(settings.getProviderName().toLowerCase()));
            if (enabled != null) {
                return BooleanUtils.toBoolean(enabled);
            }
        }
        return true;
    }

    private boolean isPersistenceCacheDisabled(GeoProviderSettings settings) {
        final String providerName = settings.getProviderName().toLowerCase();
        String value = settings.getGisParam(GisParameter.disablePersistenceCache.withProvider(providerName));
        if (value != null) {
            return BooleanUtils.toBoolean(value);
        }
        return false;//VALUE BY DEFAULT
    }

    /**
     * @return GeoRoute: double - km, long - milliseconds
     */
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public GeoRoute getFastestRoute(GeoProviderSettings settings, GeographicCoordinates from, GeographicCoordinates to) {
        if (from.equals(to)) {
            return new GeoRoute(0d, 0L, from, to);
        }

        GeoRoute route = null;
        try {
            route = tryGetFastestRoute(settings, from, to);
        } catch (Exception e) {
            if (!settings.hasFailSafeProvider()) {
                throw new RuntimeException(e);
            }
        }

        if (route == null && settings.hasFailSafeProvider()) {
            GeoProviderSettings failSafe = settings.getFailSafeProvider();

            if (isLoggingAllowed(settings)) {
                logger.debug("Route not found by using primary provider, will try use fail-safe %s %s",
                        failSafe.getProviderName(), failSafe.getVersionApi());
            }

            if (!isEqualsProviders(settings, settings.getFailSafeProvider())) {
                route = tryGetFastestRoute(failSafe, from, to);
            }
        }

        return route;
    }

    private boolean isLoggingAllowed(final GeoProviderSettings settings) {
        String enableLoggingStr = settings.getGisParam(enableLogging.withProvider(settings.getProviderName()));
        return BooleanUtils.toBoolean(enableLoggingStr);
    }

    private GeoRoute tryGetFastestRoute(GeoProviderSettings settings, GeographicCoordinates from, GeographicCoordinates to) {
        String providerName = settings.getProviderName();
        String versionApi = settings.getVersionApi();

        // check logging
        String enableLoggingStr = settings.getGisParam(enableLogging.withProvider(providerName));
        boolean loggingAllowed = BooleanUtils.toBoolean(enableLoggingStr);

        RoutesCache routesCache = null;
        Triple<Double, Long, Long> triple = null;
        boolean enabledCache = isCacheEnabled(settings);
        boolean disabledPersistenceCache = isPersistenceCacheDisabled(settings);
        Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
        if (enabledCache) {//PREPARE

            //segmentedCache.registerSegment(getProviderName, versionApi);
            routesCache = segmentedCache.getSegment(settings);
            triple = routesCache.getWithAge(from, to);
            if (triple != null && !isExpiredCacheEntry(settings, triple.getThird())) {
                GeoRoute result = getResult(triple, from, to);
                if (result != null) {
                    result.setSource(GeoRoute.Source.CACHE);
                }
                if (loggingAllowed) {
                    logger.info("Data received from cache. distance: %s, duration=%s",
                            result.getDistanceInKM(),
                            result.getDurationInMillis());
                }
                return result;
            }

            if (!disabledPersistenceCache) {//USE PERSISTED INFO
                Quadruple<Double, Long, Long, Integer> cachedInfo = persistedCache.findRouteByCID(cid, from, to);
                if (cachedInfo != null && cachedInfo.getFirst() > 0 && !isExpiredCacheEntry(settings, cachedInfo.getThird())) {
                    routesCache.put(from, to, Triple.generate(cachedInfo.getFirst(), cachedInfo.getSecond(), cachedInfo.getThird()));
                    GeoRoute result = getResult(cachedInfo, from, to);
                    if (result != null) {
                        result.setSource(GeoRoute.Source.CACHE);
                    }
                    return result;
                }
            }
        }

        IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        GeoRoute routeInfo;
        try {
            routeInfo = routingService.getRoute(from, to);
            if (routeInfo != null) {
                routeInfo.setSource(GeoRoute.Source.EXTERNAL_PROVIDER);
            }
        } catch (Exception e) {
            if (isLoggingAllowed(settings)) {
                logger.error(e.getMessage());
            }
            throw new IllegalStateException(e);
        } finally {
            GeoProviderAccessStatistics.markAccessNow(cid);
        }

        if (enabledCache) {//STORE AND PERSIST

            Double distance = routeInfo != null ? routeInfo.getDistanceInKM() : -1d;
            Long duration = routeInfo != null ? routeInfo.getDurationInMillis() : -1L;
            if (distance == 0 || duration == 0) {
                distance = (double) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
                duration = (long) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
            }
            triple = new Triple<Double, Long, Long>(distance, duration, System.currentTimeMillis());
            //в кэш памяти пишутся и пары с кодами ошибок. Планировщик умеет их обрабатывать
            routesCache.put(from, to, triple);

            if (!disabledPersistenceCache) {//PERSIST

                Integer requestDuration = (routeInfo != null ? (int) routeInfo.getRequestDuration() : 0);

                int fLat = from.getLatitudeInteger();
                int fLon = from.getLongitudeInteger();
                int tLat = to.getLatitudeInteger();
                int tLon = to.getLongitudeInteger();
                RouteRequestRecordPK pk = new RouteRequestRecordPK(providerName, versionApi, settings.getCidDiscriminator(), fLat, fLon, tLat, tLon);

                try {
                    persistedCache.persistRouteInfo(pk, Quadruple.generate(triple, requestDuration));
                    geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                } catch (IllegalStateException e) {
                    logger.error(e);
                } catch (Exception e) {
                    logger.error(e);
                }
            }
        }
        return routeInfo;
    }


    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates from,
                                                  GeographicCoordinates to, boolean calculateDistance) throws GeoException {
        List<GeoLocationRecord> routePoints;
        IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);

        String providerName = settings.getProviderName();
        String versionApi = settings.getVersionApi();
        Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());

        RoutingCoordinate source = new RoutingCoordinate(from.getLatitude(), from.getLongitude());
        RoutingCoordinate destination = new RoutingCoordinate(to.getLatitude(), to.getLongitude());
        Triple<List<GeoLocationRecord>, Long, Date> result = persistedCache.findRoutePath(cid, source, destination);

        if (result != null &&
                result.getFirst() != null &&
                result.getThird() != null &&
                !isExpiredCacheEntry(settings, result.getThird().getTime())) {
            routePoints = result.getFirst();
            if (calculateDistance) {
                calculateDistance(routePoints);
            }
            return routePoints;
        } else {
            routePoints = routingService.getRoutePoints(settings, from, to);
            if (routePoints != null && routePoints.size() > 0) {
                int fLat = source.getLatitudeInteger();
                int fLon = source.getLongitudeInteger();
                int tLat = destination.getLatitudeInteger();
                int tLon = destination.getLongitudeInteger();
                LinkedList<GeoLocationRecord> linkedList = new LinkedList<GeoLocationRecord>(routePoints);
                GeoLocationRecord first = linkedList.getFirst();
                if (!first.getLatitude().equals(from.getLatitude()) && !first.getLongitude().equals(from.getLongitude())) {
                    linkedList.addFirst(new GeoLocationRecord(from.getLongitude(), from.getLatitude()));
                }
                GeoLocationRecord last = linkedList.getFirst();
                if (!last.getLatitude().equals(to.getLatitude()) && !last.getLongitude().equals(to.getLongitude())) {
                    linkedList.addLast(new GeoLocationRecord(to.getLongitude(), to.getLatitude()));
                }
                routePoints = new ArrayList<GeoLocationRecord>(linkedList);
                if (calculateDistance) {
                    calculateDistance(routePoints);
                }
                RouteRequestRecordPK pk = new RouteRequestRecordPK(providerName, versionApi, settings.getCidDiscriminator(), fLat, fLon, tLat, tLon);
                try {
                    persistedCache.persistRoutePathInfo(pk, routePoints);
                    geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                } catch (IllegalStateException e) {
                    logger.error(e);
                } catch (Exception e) {
                    logger.error(e);
                }
            }
        }
        return routePoints;
    }

    private void calculateDistance(List<GeoLocationRecord> routePoints) {
        if (routePoints != null && routePoints.size() > 1) {
            for (int i = 1; i < routePoints.size(); i++) {
                GeoLocationRecord currentPoint = routePoints.get(i);
                GeoLocationRecord previousPoint = routePoints.get(i - 1);
                currentPoint.setDistanceToNextPoint(SpatialDataHelper.getGeoDistance(previousPoint.latitude, previousPoint.longitude, currentPoint.latitude, currentPoint.longitude));
            }
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public GeoProviderStatus updateCache(final GeoProviderSettings settings, final RoutingCoordinate from,
                                         final RoutingCoordinate to, final int delayBeforeRequests) {
        if (from.equals(to)) {
            return GeoProviderStatus.idle();
        }

        String providerName = settings.getProviderName();
        String versionApi = settings.getVersionApi();

        final Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
        final long expired = GeoProviderAccessStatistics.getLastAccess(cid) + delayBeforeRequests;
        if (System.currentTimeMillis() < expired) {
            if (isLoggingAllowed(settings)) {
                logger.debug(String.format("Provider %s is busy, wait until %d", settings.getProviderName(), expired));
            }
            return GeoProviderStatus.newBusy(expired);
        }

        Quadruple<Double, Long, Long, Integer> cachedInfo = persistedCache.findRouteByCID(cid, from, to);
        if (cachedInfo != null && cachedInfo.getFirst() > 0 && !isExpiredCacheEntry(settings, cachedInfo.getThird())) {
            return GeoProviderStatus.idle();
        }

        IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        GeoRoute routeInfo;
        try {
            routeInfo = routingService.getRoute(from, to);
        } catch (GeoException e) {
            return GeoProviderStatus.error();
        } catch (SOAPFaultException e) {
            // incorrect route?
            return GeoProviderStatus.error();
        } catch (Exception e) {
            if (isLoggingAllowed(settings)) {
                logger.error(e.getMessage());
            }
            return GeoProviderStatus.error();
        }

        // provider can't found route
        if (routeInfo == null) {
            return GeoProviderStatus.error();
        }

        Double distance = routeInfo.getDistanceInKM();
        Long duration = routeInfo.getDurationInMillis();
        if (distance == 0 || duration == 0) {
            distance = (double) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
            duration = (long) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
        }
        Quadruple<Double, Long, Long, Integer> triple = Quadruple.generate(distance, duration, System.currentTimeMillis(), (int) routeInfo.getRequestDuration());
        int fLat = from.getLatitudeInteger();
        int fLon = from.getLongitudeInteger();
        int tLat = to.getLatitudeInteger();
        int tLon = to.getLongitudeInteger();
        RouteRequestRecordPK pk = new RouteRequestRecordPK(providerName, versionApi, settings.getCidDiscriminator(), fLat, fLon, tLat, tLon);

        try {
            persistedCache.persistRouteInfo(pk, triple);
            geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
        } catch (Exception e) {
            logger.error(e);
        }

        return GeoProviderStatus.idleAfterRequest();

    }

    @Deprecated
    @Override
    public GeoProviderStatus updateCache(final GeoProviderSettings settings, final GeographicCoordinates from,
                                         final List<? extends GeographicCoordinates> to, final int delayBeforeRequests) {

        final String providerName = settings.getProviderName();
        final String versionApi = settings.getVersionApi();
        final Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
        final long expired = GeoProviderAccessStatistics.getLastAccess(cid) + delayBeforeRequests;
        if (System.currentTimeMillis() < expired) {
            if (isLoggingAllowed(settings)) {
                logger.debug(String.format("Provider %s is busy, wait until %d", settings.getProviderName(), expired));
            }
            return GeoProviderStatus.newBusy(expired);
        }

        final IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        try {
            final List<GeoRoute> routes = routingService.getRoute(from, to, 10 * 60 * 1000);

            // save into cache
            for (final GeoRoute route : routes) {
                final Double distance;
                final Long duration;
                if (route.getDistanceInKM() == 0 || route.getDurationInMillis() == 0) {
                    distance = (double) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
                    duration = (long) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
                } else {
                    distance = route.getDistanceInKM();
                    duration = route.getDurationInMillis();
                }
                final Quadruple<Double, Long, Long, Integer> quadruple = Quadruple.generate(distance, duration, System.currentTimeMillis(), (int) route.getRequestDuration());

                final GeographicCoordinates coordinateFrom = route.getFrom(), coordinateTo = route.getTo();
                final RouteRequestRecordPK pk = new RouteRequestRecordPK(
                        settings.getProviderName(), settings.getVersionApi(), settings.getCidDiscriminator(),
                        coordinateFrom.getLatitudeInteger(), coordinateFrom.getLongitudeInteger(),
                        coordinateTo.getLatitudeInteger(), coordinateTo.getLongitudeInteger());

                try {
                    persistedCache.persistRouteInfo(pk, quadruple);
                    geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                } catch (Exception e) {
                    logger.error(e);
                }
            }

        } catch (GeoException e) {
            return GeoProviderStatus.error();
        } catch (SOAPFaultException e) {
            return GeoProviderStatus.error();
        } catch (Exception e) {
            if (isLoggingAllowed(settings)) {
                logger.error(e.getMessage());
            }
            return GeoProviderStatus.error();
        }

        return GeoProviderStatus.idleAfterRequest();
    }

    private boolean isEqualsProviders(final GeoProviderSettings s1, final GeoProviderSettings s2) {
        return !(s1 == null || s2 == null) && s1.getProviderName().equals(s2.getProviderName());
    }

    private GeoRoute getResult(Triple<Double, Long, Long> triple, GeographicCoordinates from, GeographicCoordinates to) {
        return new GeoRoute(triple.getFirst(), triple.getSecond(), from, to);
    }

    private GeoRoute getResult(Quadruple<Double, Long, Long, Integer> triple, GeographicCoordinates from, GeographicCoordinates to) {
        return new GeoRoute(triple.getFirst(), triple.getSecond(), from, to);
    }


    private GeographicCoordinates getGeoCoordinate(final RoutingCoordinate coordinate) {
        return new GeographicCoordinates() {
            @Override
            public double getLatitude() {
                return coordinate.getLatitude();
            }

            @Override
            public double getLongitude() {
                return coordinate.getLongitude();
            }

            public int getLatitudeInteger() {
                return RoutingCoordinate.getIntCoord(getLatitude());
            }

            public int getLongitudeInteger() {
                return RoutingCoordinate.getIntCoord(getLongitude());
            }
        };
    }

    @Override
    public GeoRoute getRoute(GeoProviderSettings settings, List<GeographicCoordinates> coordinates) throws GeoException {
        IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        return routingService.getRoute(coordinates);
    }

    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, List<GeographicCoordinates> points) throws GeoException {
        IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        return routingService.getRoutePoints(settings, points);
    }

    private boolean existsInMemoryCache(RoutesCache routesCache, GeoProviderSettings settings, RoutingCoordinate from, RoutingCoordinate to) {
        //String providerName = settings.getProviderName();
        Triple<Double, Long, Long> triple;
        //if (routesCache != null) {
        triple = routesCache.getWithAge(from, to);
        if (triple != null && !isExpiredCacheEntry(settings, triple.getThird())) {
            return true;
        }
        //}
        return false;
    }

    private void setPersistedCacheToMemoryByPoint(RoutesCache routesCache, Long cid, final RoutingCoordinate point) {
        List<RouteRecord> routeRecords = persistedCache.findRoutes(cid, point);
        if (routeRecords == null) return;

        for (RouteRecord record : routeRecords) {
            if (routesCache != null && record.distance > 0) {
                routesCache.put(
                        record.fromLat,
                        record.fromLon,
                        record.toLat,
                        record.toLon,
                        Triple.generate(record.distance, record.duration, record.time));
            }
        }
    }

    @Override
    public void updateCache(final GeoProviderSettings settings, final RoutingCoordinate from,
                            final List<RoutingCoordinate> points) {
        //Setup parameters
        final String providerName = settings.getProviderName();
        final String versionApi = settings.getVersionApi();
        final Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
        RoutesCache routesCache = null;
        boolean enabledCache = isCacheEnabled(settings);
        boolean disabledPersistenceCache = isPersistenceCacheDisabled(settings);
        final IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        if (attemptsCountForBatchDistanceCalculation.get() == null) {
            Integer value;
            if ((value = routingService.getGisIntegerProperty(GisParameter.attempts_count_for_batch_distance_calculation)) == null) {
                value = 1;//Value by default
            }
            attemptsCountForBatchDistanceCalculation.set(value);
        }

        List<RoutingCoordinate> routingCoordinates = new ArrayList<RoutingCoordinate>();
        //Filtering cached routes
        if (enabledCache) {
            routesCache = segmentedCache.getSegment(settings);

            //Get not cached routes
            for (RoutingCoordinate destination : points) {
                if (from.equals(destination) || existsInMemoryCache(routesCache, settings, from, destination)) {
                    continue;
                }
                routingCoordinates.add(destination);
            }
            if (routingCoordinates.size() == 0) {//All routes in cache
                return;
            }

            if (!disabledPersistenceCache) {
                //Update from persisted cache (In our case from SQL database).
                setPersistedCacheToMemoryByPoint(routesCache, cid, from);
                //Remove cached routes after update
                Iterator<RoutingCoordinate> i = routingCoordinates.iterator();
                while (i.hasNext()) {
                    if (existsInMemoryCache(routesCache, settings, from, i.next())) {
                        i.remove();
                    }
                }
                if (routingCoordinates.size() == 0) {//All routes in cache
                    return;
                }
            }
        }
        try {
            int attempt = attemptsCountForBatchDistanceCalculation.get();
            Set<RoutingCoordinate> readyCoordinates = new HashSet<RoutingCoordinate>();
            List<GeoRoute> readyRoutes = new ArrayList<GeoRoute>();

            final int timeout = 10 * 60 * 100;
            int subListSize = 10;
            Integer gisIntegerProperty = routingService.getGisIntegerProperty(GisParameter.routes_batch_size_for_distance_calculation);
            if (gisIntegerProperty != null) {
                subListSize = gisIntegerProperty;
            }
            for (int i = 0; i < routingCoordinates.size(); i += subListSize) {
                List<RoutingCoordinate> subList = routingCoordinates.subList(i, Math.min(routingCoordinates.size(), i + subListSize));
                while (attempt != 0) {
                    try {
                        GeoProviderAccessStatistics.markAccessNow(cid);
                        List<GeoRoute> routes = new ArrayList<GeoRoute>();
                        if (subList.size() > 1) {
                            if (routingService.getGisBooleanProperty(GisParameter.enableBatchRouteRequests)) {
                                routes = routingService.getRoute(from, subList, timeout);
                                logger.info("getRoute: " + from + " -> " + subList.size());
                            } else {
                                routes = routingService.getRoutes(from, subList, timeout);
                            }
                        } else if (subList.size() == 1) {
                            Quadruple<Double, Long, Long, Integer> persistedResult = persistedCache.findRouteByCID(cid, from, subList.get(0));
                            if (persistedResult == null ||
                                    (persistedResult.getSecond() != null &&
                                            (persistedResult.getSecond() != null && isDecideToRequestToProvider(persistedResult.getSecond().intValue())))
                                    ) {
                                GeoRoute geoRoute = routingService.getRoute(from, subList.get(0));
                                if (geoRoute != null) {
                                    routes = Collections.singletonList(geoRoute);
                                }
                            }
                        }
                        if (routes.size() == subList.size()) { // all calculated
                            readyRoutes.addAll(routes);
                            readyCoordinates.addAll(subList);
                        }
                        break;
                    } catch (ServerUnavailableException e) {
                        attempt--;
                    }
                }
            }

            boolean enableSingleAttempts = routingService.getGisBooleanProperty(GisParameter.enable_single_attempts_for_distance_calculation);
            if (enableSingleAttempts && (attempt == 0 || readyCoordinates.size() < routingCoordinates.size())) {
                Integer attemptsCountForSingleDistanceCalculation = routingService.getGisIntegerProperty(GisParameter.attempts_count_for_single_distance_calculation);
                attempt = attemptsCountForSingleDistanceCalculation != null ? attemptsCountForSingleDistanceCalculation : 1;
                List<RoutingCoordinate> validDestinations = new ArrayList<RoutingCoordinate>(routingCoordinates);
                validDestinations.removeAll(readyCoordinates);
                while (attempt != 0) {
                    GeoProviderAccessStatistics.markAccessNow(cid);
                    List<RoutingCoordinate> invalidDestinations = new ArrayList<RoutingCoordinate>();
                    for (RoutingCoordinate destination : validDestinations) {
                        try {
                            Quadruple<Double, Long, Long, Integer> persistedResult = persistedCache.findRouteByCID(cid, from, destination);
                            if (persistedResult == null ||
                                    (persistedResult.getSecond() != null && isDecideToRequestToProvider(persistedResult.getSecond().intValue()))
                                    ) {
                                GeoRoute geoRoute = routingService.getRoute(from, destination);
                                if (geoRoute != null) {
                                    readyRoutes.add(geoRoute);
                                }
                            }
                        } catch (ServerUnavailableException e) {
                            // route not found
                            invalidDestinations.add(destination);
                        }
                    }
                    validDestinations = new ArrayList<RoutingCoordinate>(invalidDestinations);
                    attempt--;
                }
            }

            Integer persistedCacheMessageSize = routingService.getGisIntegerProperty(GisParameter.persisted_cache_message_size);
            if (persistedCacheMessageSize == null) {
                persistedCacheMessageSize = 100;
            }

            // save into cache
            ArrayList<Pair<GeographicCoordinates, Quadruple>> messageObject = new ArrayList<Pair<GeographicCoordinates, Quadruple>>();
            for (final GeoRoute route : readyRoutes) {
                final Double distance;
                final Long duration;
                if (route.getDistanceInKM() <= 0 || route.getDurationInMillis() <= 0) {
                    distance = (double) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
                    duration = (long) GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode();
                } else {
                    distance = route.getDistanceInKM();
                    duration = route.getDurationInMillis();
                }
                final GeographicCoordinates coordinateTo = route.getTo();
                final Quadruple<Double, Long, Long, Integer> quadruple = Quadruple.generate(distance, duration, System.currentTimeMillis(), (int) route.getRequestDuration());

                // put in memory cache
                if (enabledCache) {//check routesCache == null produced unexpected behavior
                    RoutingCoordinate destination = new RoutingCoordinate(
                            coordinateTo.getLatitudeInteger(),
                            coordinateTo.getLongitudeInteger()
                    );
                    routesCache.put(from, destination, Triple.generate(distance, duration, quadruple.getThird()));
                }

                if (!disabledPersistenceCache) {
                    messageObject.add(new Pair<GeographicCoordinates, Quadruple>(coordinateTo, quadruple));
                }

                if (messageObject.size() > 0 && messageObject.size() == persistedCacheMessageSize) {
                    feedPersistedCacheQueue(messageObject, from, settings.getProviderName(), settings.getVersionApi(), settings.getCidDiscriminator());
                    messageObject.clear();
                }
            }
            if (messageObject.size() > 0) {
                feedPersistedCacheQueue(messageObject, from, settings.getProviderName(), settings.getVersionApi(), settings.getCidDiscriminator());
                messageObject.clear();
            }
        } catch (Exception e) {
            if (isLoggingAllowed(settings)) {
                logger.error(e.getMessage());
            }
        } finally {
            GeoProviderAccessStatistics.markAccessNow(cid);
        }
    }

    private boolean isDecideToRequestToProvider(int errorCode) {
        return !(errorCode == GeoStatusCode.G_GEO_RESULT_IS_NULL.getCode() || errorCode == GeoStatusCode.G_GEO_ISOLATED_FINISH_POINT.getCode());
    }


    @Override
    public void updateCache(final GeoProviderSettings settings, final List<RoutingCoordinate> from, final List<RoutingCoordinate> to) {
        for (final RoutingCoordinate origin : from) {
            updateCache(settings, origin, to);
        }
    }

    @Override
    public long cleanAndGetDateOfFirstOldest(final long cid, final long maxAge) {
        final long date = System.currentTimeMillis() - maxAge;
        persistedCache.removeBeforeDate(cid, date);
        return persistedCache.getDateOfFirstOldest(cid);
    }

    @Nullable
    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(GeoProviderSettings settings, List<GeographicCoordinates> coordinates) {
        final String providerName = settings.getProviderName();
        final String versionApi = settings.getVersionApi();
        final Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
        final IGeoRouting routingService = geoProviderFacade.getGeoRouter(settings);
        try {
            return routingService.getCoordinateToCoordinateRoutes(coordinates);
        } catch (ServerUnavailableException e) {
            if (isLoggingAllowed(settings)) {
                logger.error(e.getMessage());
            }
        } finally {
            GeoProviderAccessStatistics.markAccessNow(cid);
        }
        return null;
    }

    protected void feedPersistedCacheQueue(ArrayList messageObject, RoutingCoordinate from, String providerName, String versionApi,
                                           String discriminator) {
        QueueConnection connection = null;
        try {
            connection = factory.createQueueConnection();
            final QueueSession session = connection.createQueueSession(false, QueueSession.AUTO_ACKNOWLEDGE);
            final QueueSender sender = session.createSender(persisted_cache_queue);
            ObjectMessage message = session.createObjectMessage(ArrayList.class);
            message.setObject(messageObject);
            message.setIntProperty("fromLat", from.getLatitudeInteger());
            message.setIntProperty("fromLon", from.getLongitudeInteger());
            message.setStringProperty("providerName", providerName);
            message.setStringProperty("versionApi", versionApi);
            message.setStringProperty("discriminator", discriminator);
            sender.send(message);
            sender.close();
            session.close();
        } catch (JMSException e) {
            logger.error("Message sending failed: ", e.getMessage());
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (JMSException e) {
                    logger.error(e.getMessage());
                }
            }
        }
    }
}
